package neuralNetwork.builder;

import java.util.Random;

import neuralModel.activationFunction.IActivationFunction;
import neuralModel.activationFunction.RadialActivationFunction;
import neuralModel.neuron.ClampedInputSensor;
import neuralModel.neuron.INeuron;
import neuralModel.neuron.ISignal;
import neuralModel.neuron.Neuron;
import neuralNetwork.INeuralNetwork;
import neuralNetwork.NeuralNetwork;
import neuralNetwork.NeuralNetworkUtils;

public class NetworkBuilder
{
	private int innerLayerCount;
	private int minNodesPerLayer;
	private int maxNodesPerLayer;
	private long randomSeed;
	private int inputSensorCount;
	private int outputNeuronCount;
	private int networkLayerCount;

	public NetworkBuilder()
	{

	}

	public NetworkBuilder(int inputSensorCount, int outputNeuronCount, int innerLayerCount, int minNodesPerLayer,
	    int maxNodesPerLayer, long randomSeed)
	{
		super();
		this.innerLayerCount = innerLayerCount;
		this.minNodesPerLayer = minNodesPerLayer;
		this.maxNodesPerLayer = maxNodesPerLayer;
		this.randomSeed = randomSeed;
		this.inputSensorCount = inputSensorCount;
		this.outputNeuronCount = outputNeuronCount;

		// input layer + output layer + inner layers
		this.networkLayerCount = 2 + innerLayerCount;
	}

	public INeuralNetwork buildNetwork(IActivationFunction activationFunction)
	{

		INeuralNetwork neuralNetwork = new NeuralNetwork(networkLayerCount);

		buildInputLayer(neuralNetwork, inputSensorCount);

		Random rand = new Random(randomSeed);
		for (int layer = 1; layer <= innerLayerCount; layer++)
		{
			buildInnerLayer(neuralNetwork, layer, rand.nextInt(maxNodesPerLayer - minNodesPerLayer) + minNodesPerLayer,
			    activationFunction);
		}

		buildOutputLayer(neuralNetwork, outputNeuronCount, activationFunction);

		NeuralNetworkUtils.FullyConnectNetworkLayers(neuralNetwork, 0.1);

		addBiasToNetwork(neuralNetwork, 0.1);

		randomizeWeights(neuralNetwork, .05, 1024);

		return neuralNetwork;
	}


	
	public void buildInputLayer(INeuralNetwork neuralNetwork, int nodeCount)
	{
		String nodeHeader = "input_";
		INeuron neuron;
		for (int i = 0; i < nodeCount; i++)
		{
			neuron = new ClampedInputSensor(nodeHeader + i);
			neuralNetwork.addNeuron(neuron, 0);
		}
	}

	public void buildOutputLayer(INeuralNetwork neuralNetwork, int nodeCount, IActivationFunction activationFunction)
	{
		int outputLayerIndex = neuralNetwork.getLayerCount() - 1;
		String nodeHeader = "output_";
		INeuron neuron;
		for (int i = 0; i < nodeCount; i++)
		{
			neuron = new Neuron(nodeHeader + i, activationFunction);
			neuralNetwork.addNeuron(neuron, outputLayerIndex);
		}
	}

	public void buildInnerLayer(INeuralNetwork neuralNetwork, int layerIndex, int nodeCount,
	    IActivationFunction activationFunction)
	{
		String nodeHeader = String.format("node_%d_", layerIndex);
		INeuron neuron;
		for (int i = 0; i < nodeCount; i++)
		{
			neuron = new Neuron(nodeHeader + i, activationFunction);
			neuralNetwork.addNeuron(neuron, layerIndex);
		}
	}

	public void addBiasToNetwork(INeuralNetwork neuralNetwork, double initialWeight)
	{
		INeuron biasNeuron = new ClampedInputSensor("bias");
		neuralNetwork.addNeuron(biasNeuron, 0);
		biasNeuron.setOutputValue(1.0);

		for (INeuron neuron : neuralNetwork.getNeuronMap().values())
		{
			if (neuron.getNetworkLayer() > 0)
				neuralNetwork.connectNeurons(biasNeuron, neuron, initialWeight);
		}
	}

	public void randomizeWeights(INeuralNetwork neuralNetwork, double delta, long seed)
	{
		Random rand = new Random(seed);
		double newWeight;
		for (ISignal signal : neuralNetwork.getSignalList())
		{
			newWeight = signal.getWeight();
			newWeight -= (rand.nextDouble() * delta);
			if (rand.nextInt(2) == 0)
			{
				signal.setWeight(newWeight);
			}
			else
			{
				signal.setWeight(-newWeight);
			}

		}
	}
}
